package edu.cmu.ece.ds.droidchat;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.LinkedList;
import java.util.Set;

import edu.cmu.ece.ds.droidchat.message.ClientLoginMessage;
import edu.cmu.ece.ds.droidchat.message.ClientLoginResponseMessage;
import edu.cmu.ece.ds.droidchat.message.ClientRegisterMessage;
import edu.cmu.ece.ds.droidchat.message.ClientRegisterResponseMessage;
import edu.cmu.ece.ds.droidchat.message.DroidChatMessage;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;


public class NetworkService extends Service {
	
	private static final String TAG = "NetworkService";

	private NioClient client;
	
	public void onCreate() {
		Log.d(TAG, "----- onCreate ---------");
		super.onCreate();
		client = new NioClient(this.getString(R.string.serverAip), Integer.parseInt(this.getString(R.string.serverAport)));
		Thread t = new Thread(client);
		t.start();
		Log.d(TAG, "----- thread started ---------");
	}
	
	// allow binding
	public IBinder onBind(Intent arg0) {
		Log.d(TAG, "----- OnBind---------");
		return mBinder; 
	}
	
	@Override
	public boolean onUnbind(Intent intent) {
	    Log.d(TAG, "----- OnUnbind---------");
	    return true;
	}

	
	public void onDestroy() {
		
	}
	
	private final IBinder mBinder = new LocalBinder(); 
	
	public class LocalBinder extends Binder {
		NetworkService getService() {
            // Return this instance of TCPService so clients can call public methods
            return NetworkService.this;
        }
	}
	
	/*** methods for clients ***/
	// isConnectedToServer
	public boolean isConnectedToServer(){
		return client.isConnectedToServer;
	}

	// login
	public void login(String username, String password){
		Log.d(TAG, "login");
		
		// when register, use ClientLoginMessage
		ClientLoginMessage loginMessage = new ClientLoginMessage(username, password);
		
		Log.d(TAG, "C: Sending: '" + loginMessage + "'");
		try {
			client.send(loginMessage);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	// register
	public void register(String username, String password){
		Log.d(TAG, "register");
		
		// when register, use ClinetRegisterMessage
		ClientRegisterMessage registerMessage = new ClientRegisterMessage(username, password);
		
		Log.d(TAG, "C: Sending: '" + registerMessage + "'");
		try {
			client.send(registerMessage);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	// disconnectFromServer
	public void disConnectFromServer() {

	}
	/*** end of methods for clients ***/
	
	private class NioClient implements Runnable {
		
		private Selector selector;
		private SocketChannel socketChannel;
		private LinkedList<ChangeRequest> pendingChanges;
		private LinkedList<ByteBuffer> pendingDataQueue;
		private ByteBuffer readBuffer = ByteBuffer.allocate(2048);
		private boolean isConnectedToServer;
		
		public NioClient(String ip, int port){
			try {
				selector = initSelector();
				socketChannel = connectToServer(ip, port);
				isConnectedToServer = false;
				
				pendingChanges = new LinkedList<ChangeRequest>();
				pendingDataQueue = new LinkedList<ByteBuffer>();
				
				// Queue a channel registration since the caller is not the 
			    // selecting thread. As part of the registration we'll register
			    // an interest in connection events. These are raised when a channel
			    // is ready to complete connection establishment.
				synchronized(pendingChanges) {
					pendingChanges.add(new ChangeRequest(socketChannel, ChangeRequest.REGISTER, SelectionKey.OP_CONNECT));
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		public boolean isConnected(){
			return isConnectedToServer;
		}
		
		public void send(DroidChatMessage m) throws IOException {
			if (m instanceof ClientLoginMessage) {
				Log.d(TAG, "send called " + m);
			}
			
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ObjectOutputStream oos = new ObjectOutputStream(baos);
			oos.writeObject(m);
			oos.flush();
			
		    // queue the data we want written
		    synchronized (pendingDataQueue) {
		    	pendingDataQueue.add(ByteBuffer.wrap(baos.toByteArray()));
		    }

		    Log.d(TAG, "inserted into pendingDataQueue");
		    
		    if(socketChannel.isConnected()){
		    	synchronized(pendingChanges) {
		    		this.pendingChanges.add(new ChangeRequest(socketChannel, ChangeRequest.CHANGEOPS, SelectionKey.OP_WRITE));
				}
		    }
		    
		    // Finally, wake up our selecting thread so it can make the required changes
		    this.selector.wakeup();
		}
		
		public void run() {
			try {
				while (true) {
					synchronized (pendingChanges) {
						for(ChangeRequest change: pendingChanges){
							switch (change.type) {
								case ChangeRequest.CHANGEOPS:
									SelectionKey key = change.socket.keyFor(this.selector);
									key.interestOps(change.ops);
									break;
								case ChangeRequest.REGISTER:
									change.socket.register(this.selector, change.ops);
									Log.d(TAG, "REGISTER CHANGE REQUEST ");
									break;
							}
						}
						pendingChanges.clear();
					}
					
					Log.d(TAG, "pendingDataQueue has " + pendingDataQueue.size() + " messages");
					Log.d(TAG, "calling select() ");
					int n = selector.select();
					Log.d(TAG, "select returns " + n);
					
					Set<SelectionKey> selectKeys = selector.selectedKeys();
					Log.d(TAG, "selectKeys size " + selectKeys.size());

					for (SelectionKey key: selectKeys) {
						if (key.isConnectable()) {
							Log.d(TAG, " connectable ");
							boolean result = finishConnection(key);
							
							Log.d(TAG, " connect result is " + result);
							
							if( result == true ) {
								isConnectedToServer = true;
								
								// Register an interest in writing on this channel, only if there is data to be written
								synchronized (pendingDataQueue) {
									if (!pendingDataQueue.isEmpty()) {
										Log.d(TAG, " pendingDataQueue is not empty ");
										key.interestOps(SelectionKey.OP_WRITE);
									} else {
										key.interestOps(SelectionKey.OP_READ);
									}
								}
							} else {
								// connection failed, try next server
								
							}
							
						} else if (key.isWritable()) {
							Log.d(TAG, " writable ");
							write(key);
						} else if (key.isReadable()) {
							Log.d(TAG, " readable ");
							read(key);
						}
						Log.d(TAG, " removing key ");
						selectKeys.remove(key);
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		public void write(SelectionKey key) throws IOException {
			SocketChannel socketChannel = (SocketChannel) key.channel();
			
			synchronized (pendingDataQueue) {
				while (!pendingDataQueue.isEmpty()) {
					ByteBuffer buf = (ByteBuffer) pendingDataQueue.get(0);
					socketChannel.write(buf);
					if (buf.remaining() > 0) {
						// ... or the socket's buffer fills up
						break;
					}
					pendingDataQueue.remove(0);
				}
				
				if (pendingDataQueue.isEmpty()) {
					// We wrote away all data, so we're no longer interested
					// in writing on this socket. Switch back to waiting for data.
					key.interestOps(SelectionKey.OP_READ);
				}
			}
			
		}

		public void read(SelectionKey key) throws IOException {
			Log.d(TAG, " read() called ");
			
			SocketChannel socketChannel = (SocketChannel) key.channel();
			
			// Clear out our read buffer so it's ready for new data [otherwise you get stale data]
			readBuffer.clear();
			
			// Attempt to read off the channel
			int numRead;
			
			try {
				numRead = socketChannel.read(readBuffer);
			} catch (IOException e) {
				// The remote forcibly closed the connection, cancel
				// the selection key and close the channel.
				key.cancel();
				socketChannel.close();
				isConnectedToServer = false;
				return;
			}
			
			if (numRead == -1) {
				// Remote entity shut the socket down cleanly. Do the
				// same from our end and cancel the channel.
				key.channel().close();
				key.cancel();
				isConnectedToServer = false;
				return;
			}else{
				Log.d(TAG, numRead + " bytes read");
				
				// turn bytes into DroidChatMessage
				ByteArrayInputStream bais = new ByteArrayInputStream(readBuffer.array());
				ObjectInputStream ois = new ObjectInputStream(bais);
				DroidChatMessage m = null;
				try {
					m = (DroidChatMessage) ois.readObject();
				} catch (ClassNotFoundException e) {e.printStackTrace();}
				// Handle the response
				handleResponse(m);
			}
		}
		
		// upon receiving a DroidChatMessage from DroidChat server, call this method
		private void handleResponse(DroidChatMessage m) {
			if (m instanceof ClientRegisterResponseMessage) {
				ClientRegisterResponseMessage mm = (ClientRegisterResponseMessage) m;
				Log.d(TAG, " ClientRegisterResponseMessage=" + mm);
				Log.d(TAG, " in handleResponse(): " + mm.getUsername());
				broadcastRegisterResponse(mm);
			}else if(m instanceof ClientLoginResponseMessage) {
				ClientLoginResponseMessage mm = (ClientLoginResponseMessage) m;
				Log.d(TAG, " ClientLoginResponseMessage=" + mm);
				Log.d(TAG, " in handleResponse(): " + mm.getUsername());
				broadcastLoginResponse(mm);
			}
		}

		// send broadcast for ClientLoginResponseMessage received
		private void broadcastLoginResponse(ClientLoginResponseMessage mm) {
			Intent intent = new Intent("edu.cmu.ece.ds.droidchat.message.ClientLoginResponse");
			intent.putExtra("username", mm.getUsername());
			intent.putExtra("response", mm.getResponse());
			intent.putExtra("login_session_id", mm.getLoginSessionID());
			Log.d(TAG, " broadcastLoginResponse() is called ");
			sendBroadcast(intent);
		}

		// send broadcast for ClientRegisterResponseMessage received
		private void broadcastRegisterResponse(ClientRegisterResponseMessage mm) {
			Intent intent = new Intent("edu.cmu.ece.ds.droidchat.message.ClientRegisterResponse");
			intent.putExtra("username", mm.getUsername());
			intent.putExtra("response", mm.getResponse());
			Log.d(TAG, " broadcastRegisterResponse() is called ");
			sendBroadcast(intent);
		}

		private Selector initSelector() throws IOException {
			// Create a new selector
			Selector socketSelector = SelectorProvider.provider().openSelector();
			return socketSelector;
		}
		
		private SocketChannel connectToServer(String ip, int port) throws IOException {
			// Create a non-blocking socket channel
			SocketChannel socketChannel = SocketChannel.open();
			socketChannel.configureBlocking(false);
			
			// Kick off connection establishment
			socketChannel.connect(new InetSocketAddress(ip, port));
			return socketChannel;
		}
		
		private boolean finishConnection(SelectionKey key) throws IOException {
			SocketChannel socketChannel = (SocketChannel) key.channel();
		  
			boolean connected = false;
			
			// Finish the connection. If the connection operation failed
			// this will raise an IOException.
			try {
				connected = socketChannel.finishConnect();
			} catch (IOException e) {
				// Cancel the channel's registration with our selector
				key.cancel();
			}

			return connected;
		}
		
	}
}
